/**
 * Copyright (c) 2024 Hunan OpenValley Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "GPURenderThread.h"
#include "GPUImageHueFilter.h"
#include "GPUImagePixelationFilter.h"
#include "OpenGlUtils.h"
#include "TextureRotationUtil.h"
#include "napi_tool.h"
#include <EGL/eglplatform.h>
#include <GLES2/gl2ext.h>
#include <bits/alltypes.h>
#include <native_window/external_window.h>
#include <sstream>
#include <unistd.h>

GPURenderThread::GPURenderThread() : running(false)
{
    eglSurface = EGL_NO_SURFACE;
}

GPURenderThread::~GPURenderThread()
{
    destroyRenderContext();
    destroyNativeImage();
}

void GPURenderThread::start(void *nativeWindow, uint64_t width,
                            uint64_t height)
{
    if (running) {
        runOnDraw([this, nativeWindow, width, height]() {
            initRenderContext(nativeWindow, width, height);
        });
        return;
    }
    if (!createNativeImage()) {
        return;
    }
    running = true;
    renderThread = std::thread([this, nativeWindow, width, height]() {
        if (!initRenderContext(nativeWindow, width, height)) {
            return;
        }
        if (!createGLResources()) {
            return;
        }
        threadMainLoop();
    });
}

void GPURenderThread::stop()
{
    if (running.load()) {
        running.store(false);
        availableFrameCnt++;
        renderThread.detach();
    }
}

void GPURenderThread::setFilter(std::shared_ptr<GPUImageFilter> switchFilter)
{
    std::lock_guard<std::mutex> lock(filterMutex);
    if (filter) {
        filter->destroy();
        filter.reset();
    }
    filter = switchFilter;
    runOnDraw([this]() {
        std::lock_guard<std::mutex> lock(filterMutex);
        filter->ifNeedInit();
        filter->onOutputSizeChanged(outputWidth, outputHeight);
    });
}

void GPURenderThread::setCallback(std::shared_ptr<CallbackListener> callback)
{
    filterOutputCb = callback;
}

uint64_t GPURenderThread::getSurfaceId() { return surfaceId; }

void GPURenderThread::onNativeImageFrameAvailable(void *data)
{
    GPURenderThread *renderThread = (GPURenderThread *)data;
    if (renderThread == nullptr) {
        LOGE("onNativeImageFrameAvailable renderThread is null");
        return;
    }
    renderThread->notifyFrameAvailable();
}

void GPURenderThread::notifyFrameAvailable()
{
    availableFrameCnt++;
    wakeUpCond.notify_one();
}

void GPURenderThread::updateXComponentSize(int w, int h)
{
    outputWidth = w;
    outputHeight = h;
    if (filter) {
        filter->onOutputSizeChanged(outputWidth, outputHeight);
    }
}

float GPURenderThread::addDistance(float coordinate, float distance)
{
    return coordinate == 0.0f ? distance : 1 - distance;
}

void GPURenderThread::renderFrame()
{
    if (eglSurface == EGL_NO_SURFACE) {
        LOGE("eglSurface is EGL_NO_SURFACE");
        return;
    }
    renderContext->makeCurrent(eglSurface);
    if (textureOES == OpenGlUtils::NO_TEXTURE) {
        configTexture();
    }
    runPendingOnDrawTasks();

    OH_NativeImage_AttachContext(nativeImage, textureOES);
    int32_t ret = OH_NativeImage_UpdateSurfaceImage(nativeImage);
    if (ret != 0) {
        LOGE("OH_NativeImage_UpdateSurfaceImage err, code:%{public}d", ret);
        return;
    }
    filter->onDraw(textureOES, glCubeBuffer, glTextureBuffer);

    if (isNeedDump) {
        void *pixels = new GLubyte[outputWidth * outputHeight * 4];
        glReadPixels(0, 0, outputWidth, outputHeight, GL_RGBA, GL_UNSIGNED_BYTE,
                     pixels);
        dumpImage(pixels, outputWidth, outputHeight);
        delete[] pixels;
    }
    if (isNeedSavePixelMap) {
        readPixelFromFBO();
    }
}

void GPURenderThread::threadMainLoop()
{
    while (running) {
        {
            std::unique_lock<std::mutex> lock(wakeUpMutex);
            wakeUpCond.wait(lock, [this]() {
                return availableFrameCnt > 0;
            });
        }
        if (availableFrameCnt <= 0) {
            continue;
        }
        renderFrame();
        renderContext->swapBuffers(eglSurface);
        availableFrameCnt--;
    }
}

bool GPURenderThread::initRenderContext(void *nativeWindow, uint64_t width,
                                        uint64_t height)
{
    outputWidth = width;
    outputHeight = height;
    renderContext = std::make_unique<EglRenderContext>();
    bool ret = renderContext->initContext(nativeWindow, width, height);
    if (ret) {
        (void)OH_NativeWindow_NativeWindowHandleOpt(
            reinterpret_cast<OHNativeWindow *>(nativeWindow), SET_BUFFER_GEOMETRY,
            static_cast<int>(width), static_cast<int>(height));
        eglSurface = renderContext->createEglSurface();
    }
    return ret;
}

void GPURenderThread::destroyRenderContext() {}

bool GPURenderThread::createGLResources()
{
    renderContext->makeCurrent(eglSurface);
    setFilter(std::make_shared<GPUImageFilter>());
    return true;
}

void GPURenderThread::cleanGLResources()
{
    if (textureOES != 0) {
        glDeleteTextures(1, &textureOES);
        textureOES = 0;
    }
    if (eglSurface != EGL_NO_SURFACE) {
        renderContext->destroyEglSurface(eglSurface);
    }
}

bool GPURenderThread::createNativeImage()
{
    nativeImage = OH_NativeImage_Create(-1, GL_TEXTURE_EXTERNAL_OES);
    if (nativeImage == nullptr) {
        LOGE("OH_NativeImage_Create failed");
        return false;
    }
    int ret = OH_NativeImage_GetSurfaceId(nativeImage, &surfaceId);
    if (ret != 0) {
        LOGE("OH_NativeImage_GetSurfaceId failed, ret:%{public}d", ret);
        return false;
    }

    onFrameAvailableListener.onFrameAvailable =
        &GPURenderThread::onNativeImageFrameAvailable;
    onFrameAvailableListener.context = this;
    ret = OH_NativeImage_SetOnFrameAvailableListener(nativeImage,
                                                     onFrameAvailableListener);
    if (ret != 0) {
        LOGE("OH_NativeImage_SetOnFrameAvailableListener failed, ret:%{public}d",
             ret);
        return false;
    }
    return true;
}

void GPURenderThread::destroyNativeImage()
{
    if (nativeImage != nullptr) {
        OH_NativeImage_UnsetOnFrameAvailableListener(nativeImage);
        OH_NativeImage_Destroy(&nativeImage);
        nativeImage = nullptr;
    }
}

void GPURenderThread::configTexture()
{
    if (outputWidth == 0 || outputHeight == 0) {
        LOGE("error outputWidth and outputHeight!");
    }
    if (textureOES == OpenGlUtils::NO_TEXTURE) {
        glGenTextures(1, &textureOES);
        glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureOES);
        // set the texture wrapping parameters
        glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_REPEAT);
        // set texture filtering parameters
        glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0);
        if (textureOES == OpenGlUtils::NO_TEXTURE) {
            LOGE("glGenTextures error!");
        }
    }
}

void GPURenderThread::savePixelMap(std::shared_ptr<CallbackListener> callback)
{
    filterOutputCb = callback;
    isNeedSavePixelMap = true;
}

void GPURenderThread::readPixelFromFBO()
{
    std::lock_guard<std::mutex> lk(taskMutex);
    void *pixels = new GLubyte[outputWidth * outputHeight * 4];
    glReadPixels(0, 0, outputWidth, outputHeight, GL_RGBA, GL_UNSIGNED_BYTE,
                 pixels);
    // 使用NativeImage把相机预览流更新到纹理后，从FBO读出来的YUV420SP格式的相机预览帧已经被转成RGBA格式
    // 但是buffer如果设置解码成RGBA的PixelMap会偏色
    OpenGlUtils::ConvertRGBAtoBGRA(pixels, outputWidth * outputHeight);
    filterOutputCb->pixelMap.ops.width = outputWidth;
    filterOutputCb->pixelMap.ops.height = outputHeight;
    filterOutputCb->pixelMap.ops.alphaType = 0;
    uint8_t rgba = 3;
    filterOutputCb->pixelMap.ops.pixelFormat =
        rgba; // pixelFormat指定为要把buffer解码成什么像素格式
    if (filterOutputCb->pixelMap.buffer) {
        delete[] filterOutputCb->pixelMap
            .buffer; // PixelMap解码传入的像素Buffer会被当成BGRA格式处理
        filterOutputCb->pixelMap.buffer = nullptr;
    }
    filterOutputCb->pixelMap.buffer = pixels;
    filterOutputCb->OnCallbackAsync();
    isNeedSavePixelMap = false;
}

void GPURenderThread::dumpImageRequest(const std::string &path)
{
    isNeedDump = true;
    dumpPath = path;
}

void GPURenderThread::dumpImage(void *data, int width, int height)
{
    OpenGlUtils::SaveRaw("/data/storage/el2/base/cache", frameId, (uint8_t *)data,
                         width, height);
    frameId++;
    isNeedDump = false;
}

void GPURenderThread::runOnDraw(std::function<void()> task)
{
    std::unique_lock<std::mutex> lock(taskMutex);
    tasks.emplace(std::move(task));
}

void GPURenderThread::runPendingOnDrawTasks()
{
    std::unique_lock<std::mutex> lock(taskMutex);
    while (!tasks.empty()) {
        std::function<void()> task = std::move(tasks.front());
        tasks.pop();
        lock.unlock();
        task();
        lock.lock();
    }
}

void GPURenderThread::releaseResources()
{
    stop();
    destroyNativeImage();
    cleanGLResources();
    if (renderContext != nullptr) {
        renderContext = nullptr;
    }
    if (filterOutputCb) {
        filterOutputCb = nullptr;
    }
}

void GPURenderThread::setOrientation(Rotation rotation, bool flipX,
                                     bool flipY)
{
    if (flipX && !flipY) {
        memcpy(glCubeBuffer, FLIP_X_CUBE, sizeof(FLIP_X_CUBE));
    } else if (flipY && !flipX) {
        memcpy(glCubeBuffer, FLIP_Y_CUBE, sizeof(FLIP_Y_CUBE));
    } else if (flipX && flipY) {
        memcpy(glCubeBuffer, FLIP_X_Y_CUBE, sizeof(FLIP_X_Y_CUBE));
    } else {
        memcpy(glCubeBuffer, NORMAL_CUBE, sizeof(NORMAL_CUBE));
    }
}
